Дашборд патчей: для одного или нескольких koji-тегов собирает последние билды
(с учётом наследования тегов), по extra.source.original_url определяет
ветку GitLab, с которой собран каждый билд, читает в этой ветке каталог
PATCH, классифицирует найденные файлы (AUTOGEN / CVE / SAST / DAST /
COVERAGE / DISTSUFFIX / LICENSE / SPEC / CHANGELOG / FILES / other) и
показывает всё это в HTML-дашборде с вкладкой «Состояние» по каждому тегу и вкладкой «Изменения»,
сравнивающей теги между собой.
Данные и представление в проекте разделены, и делают их разные команды.
collect ходит в koji и GitLab и кладёт снапшот тега в JSON — это данные, и
только они. page кладёт на диск страницу, внутри которой нет ни одного
снапшота, — это представление, и только оно. Снапшоты в страницу подгружает
человек, уже открыв её в браузере.
Разделение не ради стройности: снапшоты, которые захочется сравнить, обычно собраны в разные дни и лежат в разных файлах, а какие именно файлы человек положит рядом, в момент сборки страницы не знает никто. Готовым такой дифф взяться неоткуда — считать его некому, кроме самой страницы. Поэтому дашборд считает и дифф, и счётчики, и порядок строк сам, из того, что в него подгрузили.
Страница — один HTML-файл без внешних зависимостей: CSS и JS встроены в неё, сеть ей не нужна, работает при открытии прямо с диска. Пересобирать её при появлении новых снапшотов не надо — она от данных не зависит.
- Python 3.9+
PyYAML— разбор конфига; нужен всегда, модуль конфигурации импортируется при любом запускеkoji— клиент XML-RPC к хабу; нужен только дляcollect, импортируется внутри самой командыrequests— HTTP-запросы к GitLab REST v4; нужен только дляcollect, импортируется лениво внутри транспорта, который дёргает сеть только при сборе
page не требует ни koji, ни requests, ни даже конфига: страница
пуста, пока в неё не подгрузят снапшоты, и ходить ей некуда.
Все модули должны быть доступны в интерпретаторе, которым запускается
дашборд; отдельного requirements.txt в проекте нет. Поставить
недостающее можно, например, через pip install koji requests pyyaml —
пакет koji на PyPI и есть официальный клиент проекта Koji.
Дашборд запускается двумя способами, и оба равноправны.
Из каталога с исходниками, без установки вовсе:
python3 -m dashboard --versionИли пакетом, тогда появляется команда dashboard:
pip install --no-build-isolation -e .
dashboard --version--no-build-isolation здесь не прихоть: без него pip уходит в сеть за
свежим setuptools, а на машине сборки её может не быть. Метаданные
пакета лежат в setup.cfg, а не в [project] внутри pyproject.toml:
[project] понимают только setuptools от 61-й версии, тогда как в
RHEL 9 стоит 53-я.
Зависимости в метаданных не перечислены нарочно: koji, requests и
PyYAML ставят системным пакетным менеджером, где они собраны под тот же
питон, что и сам koji-клиент. Перечисли мы их здесь — pip дрался бы с
дистрибутивом за одни и те же файлы.
Дальше в README везде написано python3 -m dashboard; если пакет
установлен, вместо этого работает просто dashboard.
export GITLAB_TOKEN=glpat-... # опц., см. «Токен GitLab»
cp dashboard.example.yaml dashboard.yaml # поправить адреса под себя
python3 -m dashboard --config dashboard.yaml collect \
--tag os-9.2 -o os-9.2.json
python3 -m dashboard page -o dashboard.htmlДальше dashboard.html открывают в браузере и перетаскивают в него
os-9.2.json. С одним снапшотом работает вкладка «Состояние».
Сравнить два тега — собрать оба снапшота:
python3 -m dashboard --config dashboard.yaml collect \
--tag os-9.1 --tag os-9.2 -o snapshots.jsonНесколько --tag пишут в один файл список снапшотов, так что подгрузить его
можно одним файлом. Как только на странице оказалось два снапшота, появляется
вкладка «Изменения».
Сравнить тег с ним же месяц назад — те же два снапшота, только собранные в разное время:
python3 -m dashboard --config dashboard.yaml collect --tag os-9.2 \
-o os-9.2-07-01.json
# ... через месяц ...
python3 -m dashboard --config dashboard.yaml collect --tag os-9.2 \
-o os-9.2-08-01.jsonОба файла подгружаются в ту же самую страницу: снапшот опознаётся парой «тег и время сбора», поэтому два прогона одного тега страница различает и сравнивает между собой.
Посмотреть дашборд, не имея доступа к koji, можно на снапшотах из тестов. Это обычные файлы снапшотов, они подгружаются так же, как собранные вами:
tests/fixtures/rich-old.json os-9.1
tests/fixtures/rich-new.json os-9.2
tests/fixtures/rich-newer.json os-9.3
tests/fixtures/rich-newest.json os-9.4
tests/fixtures/rich-again.json os-9.4, собранный месяцем позже
tests/fixtures/rich-mirror.json os-9.5
tests/fixtures/rich-wide.json os-9.6
tests/fixtures/rich-many.json os-9.7
Берите все восемь: каждый следующий показывает то, чего не показывают предыдущие. В них нарочно собрано то, ради чего дашборд и написан — все классы патчей, унаследованные и затегованные прямо билды, компонент с неизвестным тегом, подпакеты по четырём архитектурам, ошибка GitLab и внутренняя ошибка.
На двух снапшотах не видно ни сводной пары, ни рельса из трёх узлов.
Третий добавляет случаи, которые на двух не показать: vim уходил в
os-9.2 и вернулся тем же билдом, zlib откатывался и поднялся обратно на
прежний релиз. По шагам оба двигались, а сводная пара os-9.1 → os-9.3
скажет, что ничего не изменилось, — за этим она и нужна.
Четвёртый добавляет своё. Патчи класса DISTSUFFIX — в первых трёх их нет
вовсе, а один из них нарочно назван kernel.spec.distsuffix.patch, чтобы
было видно: файл со «спековым» именем всё равно уходит в DISTSUFFIX.
Диапазон, который не сводный и не соседний, — на трёх узлах такого нет
вовсе, там os-9.1 → os-9.3 и есть вся цепочка; здесь curl, появившийся в
os-9.2 и исчезнувший в os-9.3, возвращается тем же билдом, и os-9.2 →
os-9.4 честно говорит «не изменилось», пока сводный os-9.1 → os-9.4 говорит
«появился». И время: os-9.4 собран через восемь часов после os-9.3, а не
через месяц, — на рельсе видно, что расстояние между узлами меряется той
единицей, которая ему подходит.
Пятый — тот же тег os-9.4, собранный месяцем позже. Снапшот опознаётся
парой «тег и время сбора», и на рельсе это два узла с одним именем: пара из
них отвечает на вопрос «что за месяц случилось с тегом», а не «чем один тег
отличается от другого». Внутри — httpd, пересобранный другим владельцем
из переехавшей в другую группу GitLab ветки: ни владелец, ни проект своей
метки не имеют, их показывает раскрытая строка. Там же zlib,
пересобранный руками из готового SRPM: ветки у такого билда нет, каталог
PATCH читать негде, и пара «ветка → srpm» в раскрытии «Изменений»
показана как есть. И openssl без владельца и без времени сборки —
колонке «владелец» тоже есть что показать пустой.
Шестой собран с другого koji-хаба, и страница об этом предупреждает окошком: сравнивать билды разных хабов обычно бессмысленно, но бывает и наоборот — переезд, зеркало, — поэтому снапшот принимается, а не отвергается. Заодно видно, что ссылка на билд ведёт на хаб того снапшота, из которого билд приехал: в паре os-9.4 → os-9.5 левая сторона ведёт на прежний хаб, правая — на зеркало.
Седьмой — про то, обо что таблица разъезжается. У chromium в нём патчи
всех классов разом, обе ошибки, унаследованный тег и сборка с коммита:
полтора десятка меток в одной ячейке, сорокасимвольный хеш, длинный путь
проекта и два десятка подпакетов по четырём архитектурам. В живом теге
такой билд один на сотню, и попадается он позже, чем правят вёрстку.
Восьмой — тег величиной с настоящий, сто пять билдов. Сортировка меняет порядок сотни строк, поиск отсекает, а не подсвечивает одну, числа на карточках и в меню фильтров перестают быть однозначными, «развернуть все» разворачивает сотню карточек. Патчи, проблемы, наследование, сборка с коммита и отсутствие исходников расставлены по кругу, чтобы попадаться вперемешку.
Класс DISTSUFFIX встаёт в карточках последним, за other, и это не сбой:
список классов страница складывает из всех загруженных снапшотов, а в
первых трёх его нет — их patch_classes оставлен прежним нарочно, из них
порождён эталон page-data.golden.json, пересобрать который уже нечем.
Так же выглядела бы страница, на которую положили снапшоты, собранные
разными версиями dashboard: класс, известный не всем, дописывается в
конец. Тем же хвостом встаёт и LICENSE — он появился ещё позже, и его
знают только четыре последних снапшота.
Рядом лежат snapshot-os-9.1.json и snapshot-os-9.2.json: они старого
формата, без patch_classes и тегов билдов, и годятся разве что затем,
чтобы посмотреть, как страница читает снапшот прежней версии.
--config можно не передавать флагом, а задать переменной окружения
DASHBOARD_CONFIG.
python3 -m dashboard --config dashboard.yaml collect \
--tag os-9.1 --tag os-9.2 -o snapshots.json--tag можно указывать несколько раз — тогда в выходной файл будет записан
список снапшотов, по одному на тег. Требует koji.hub (в конфиге или через
--koji-hub). Без -o пишет snapshot.json.
python3 -m dashboard page -o dashboard.htmlСобирает шаблон и скрипты в один самодостаточный файл и записывает его.
Данных внутри нет, поэтому ни koji, ни GitLab, ни конфиг команде не нужны:
пример выше отработал без --config. Без -o пишет dashboard.html.
Пересобирать страницу нужно только после обновления самого dashboard — новые снапшоты в неё просто подгружают.
| Флаг | Что делает |
|---|---|
--config PATH |
путь к YAML-конфигу (или DASHBOARD_CONFIG в окружении) |
--koji-hub URL |
перекрыть koji.hub из конфига |
--gitlab-api URL |
перекрыть адрес GitLab API (пишется как хост default_host или *, если default_host не задан) |
--patch-dir NAME |
перекрыть имя каталога патчей (по умолчанию PATCH) |
--jobs N |
параллельных запросов к GitLab при сборе, по умолчанию 8 |
--max-problems N |
см. «Коды возврата» — учитывается только у collect |
--log-level {error,warning,info,debug} |
подробность лога, по умолчанию info; см. «Логи» |
--version |
напечатать версию и выйти; подкоманда при этом не нужна |
Эти флаги общие для всего CLI и должны идти до имени подкоманды
(dashboard --log-level debug collect --tag ..., а не
dashboard collect --log-level debug --tag ...) — таковы правила
argparse-подпарсеров, используемых в проекте.
Конфиг — YAML-файл, пример в dashboard.example.yaml. Все ключи, кроме
koji.hub (для collect) и gitlab.hosts.<host>.api (для каждого
описанного хоста), необязательны.
| Ключ | Обязателен | По умолчанию | Что делает |
|---|---|---|---|
koji.hub |
да для collect, не нужен для page |
— | XML-RPC адрес хаба; можно перекрыть --koji-hub |
koji.web |
нет | нет ссылок на koji в дашборде | база для ссылок вида /search?match=exact&type=build&terms=NVR |
gitlab.default_host |
нет | — | хост, на который будет отправлен запрос, если хост из original_url не описан в gitlab.hosts |
gitlab.token_env |
нет | GITLAB_TOKEN |
имя переменной окружения, из которой читается токен GitLab |
gitlab.hosts.<host>.api |
да для каждого описанного хоста | — | база REST v4, например https://gitlab.example.com/api/v4 |
gitlab.hosts.<host>.web |
нет | api до /api/... |
база для веб-ссылок на дерево и файлы репозитория |
patch_dir |
нет | PATCH |
имя каталога патчей в корне репозитория; можно перекрыть --patch-dir |
patch_classes |
нет | правила для AUTOGEN, CVE, SAST, DAST, COVERAGE, DISTSUFFIX, LICENSE, SPEC, CHANGELOG, FILES + other: '.*' |
список правил {name, pattern} — регулярка по имени файла патча → класс |
Правила patch_classes применяются по порядку, побеждает первое совпадение
(re.search, без привязки к началу строки). Первым идёт AUTOGEN — имена,
начинающиеся с autogen- или autogen_ (autogen-sast-patches.inc.new).
Это сгенерированные перечни патчей, а не патчи, и стоять правило обязано
именно первым: в самих этих именах есть sast, cve и fuzz, так что ниже
по списку файлы уехали бы в SAST и DAST и накрутили их счётчики. Привязка к
началу имени тоже не случайна — httpd-autogen-fix.patch.new это обычный
патч.
Дальше CVE опознаётся по идентификатору в любом месте имени
(httpd-2.4.62-cve-2024-42516.patch.new),
SAST, DAST и COVERAGE — по вхождению маркера, тоже в любом месте
(SAST-src.core.ngx_file.c.patch.new и
httpd-2.4.62-sast-src.core.c.patch.new оба попадут в SAST,
COVERAGE-parser.patch.new — в COVERAGE). В DAST кроме
dast попадает и fuzz (FUZZ-parser.patch.new,
httpd-2.4.62-fuzz-parser.patch.new): фаззинг — то же динамическое
тестирование, и отдельной категорией он бы только дробил отчёт.
DISTSUFFIX — по distsuffix.patch (nginx-distsuffix.patch,
kernel.spec.distsuffix.patch): такой патч переклеивает суффикс сборки, и
правило стоит выше SPEC — правит он и правда спек, но класс отвечает на
вопрос «зачем патч». Расширение в правиле обязательно, иначе в класс уехал
бы и distsuffix.inc.
LICENSE — по слову license в любом месте имени (nginx-license.patch,
LICENSE.txt, британское licence тоже): такой патч обычно правит поле
License в спеке, и правило стоит выше SPEC по той же причине, что и
предыдущее. Расширения оно не требует — файл с этим словом в каталоге
патчей про лицензию и есть, чем бы он ни был.
SPEC опознаётся по .spec. с точками
(nginx.spec.patch, httpd-2.4.62.spec.patch.new), CHANGELOG — по
changelog.yaml (короткое .yml тоже ловится), а FILES — по .tar.gz.
Точки в правиле SPEC обязательны: без них в класс попали бы
specialcase.patch и respec-fix.patch.
Имя, где есть и CVE-идентификатор, и маркер SAST, уйдёт в CVE: правило CVE
стоит выше. По той же причине httpd-cve-2024-42516.spec.patch — это CVE,
а не SPEC, а cve-2024-42516-sources.tar.gz — CVE, а не FILES: класс
отвечает на вопрос «зачем файл», а не «какого он вида».
Расширение роли не играет — классифицируется всё имя файла целиком, так что
.patch, .patch.new и любое другое равнозначны.
FILES — это не патчи, а просто файлы, лежащие в том же каталоге. Категория
задумана расширяемой: когда встретится ещё одно расширение, дописывайте его
в то же правило альтернативой ('(?i)\.(?:tar\.gz|zip|bin)'), а не
заводите новый класс. Цветов, которые человек различает в тонкой полоске
состава патчей, конечное число, и восемь классов — это уже предел; каждый
следующий отбирает различимость у остальных.
Если понадобится строже, чтобы маркер не ловился внутри слова, замените правило
на '(?i)(?:^|[^a-z0-9])sast(?:[^a-z0-9]|$)'. Если последнее правило не
всеохватное, к списку автоматически добавляется other: '.*', так что любой
файл в каталоге патчей всегда получает класс. Разделы koji, gitlab и
gitlab.hosts.<host> при разборе проверяются на форму: если вместо
отображения ({...}) там оказалась строка или список, load_config
поднимает ConfigError с понятным сообщением, а не падает трейсбеком.
Имена классов в порядке правил уезжают в снапшот полем patch_classes — у
дашборда конфига нет, а порядок карточек и меток он берёт именно оттуда (см.
«Формат снапшота»).
Токен читается только из переменной окружения (имя задаётся gitlab.token_env,
по умолчанию GITLAB_TOKEN) — специального флага для него нет, чтобы он не
попадал в историю шелла и в список процессов:
export GITLAB_TOKEN=glpat-...Токен опционален. Без него запросы к GitLab идут анонимно — рабочий режим
для публичных репозиториев; для приватных 401/403 в ответ на запрос
дерева PATCH не роняют весь прогон, а записываются в problems
конкретного билда (см. «Формат снапшота»), и такой билд помечается в
дашборде тегом gitlab-error.
Свежая страница показывает только зону загрузки: вкладки без данных обещали бы содержимое, которого нет. Файлы снапшотов в неё либо перетаскивают (ронять можно на всё окно, не только на зону), либо выбирают кнопкой; и то, и другое принимает сразу несколько файлов. Третьего способа нет намеренно: страницу открывают с диска, а браузер запрещает ей читать соседние файлы по адресу, так что дверей ровно две — те, где файл даёт сам человек.
Когда снапшоты уже есть, добавляют их там же, где показаны: в конце рельса стоит пунктирный «+ добавить», открывающий тот же диалог. Ронять файлы на страницу можно по-прежнему.
Снапшоты выстраиваются в цепочку по времени сбора, от раннего к позднему: именно в этом порядке они сравниваются попарно, и он же виден на рельсе над вкладками. Рельс — главный орган страницы: узел на нём это снапшот, под тегом стоит время сбора, а над отрезком между соседними узлами — расстояние между ними во времени («7 ч», «31 дн», «2 мес»). Единица крупная нарочно: точные даты и так стоят под узлами, а от подписи нужен порядок величины. Длинная цепочка рельс не переносит, а прокручивает: перенос оставил бы на конце строки отрезок, ведущий в пустоту. Прокручивают колесом мыши, наведя курсор на рельс, — полосы прокрутки под ним нет: рельс сам линия, и вторая линия под ней читалась бы как часть рисунка. Докрученная до конца цепочка колесо странице не отдаёт: чтобы уехать со страницей, курсор с рельса отводят — полоса у него узкая. Отрезок при этом не ужимается уже своей подписи: длинная цепочка уезжает за край, но «31 дн» над отрезком не наползает на соседние узлы.
Составом управляют на самом рельсе. Порядок меняют перетаскиванием узла: пока узел едет, сосед, к которому его подносят, показывает полосой, с какой стороны тот встанет. Состоявшаяся перестановка отключает автоматическую сортировку до конца жизни страницы: раз человек сказал, в каком порядке сравнивать, дальше подгруженные файлы дописываются в конец цепочки и порядок больше никто не трогает. Убирает снапшот крестик в углу узла — он проявляется при наведении и при фокусе. Убрать можно и последний снапшот: страница тогда возвращается к зоне загрузки. Число билдов и имя файла, из которого снапшот приехал, стоят в подсказке узла.
Один и тот же снапшот дважды не загрузится: снапшот опознаётся парой «тег и время сбора», и повторная попытка отклоняется с сообщением, а не удваивает цепочку. Из той же пары следует, что два прогона одного тега — законный случай: они различимы, и на рельсе их различает время сбора, стоящее под каждым тегом.
Отказы называются по имени файла и всплывают окошком в правом нижнем углу: не разбирается как JSON, чужая версия схемы, «это не снапшот dashboard», файл не читается. Одно окошко на всю пачку файлов, внутри — по строке на причину; длинный список режется, и последняя строка говорит, сколько причин скрыто. Окошко гаснет через десять секунд само, но под курсором отсчёт стоит, а крестик убирает его сразу. Больше четырёх окошек на экране не собирается: пятое выталкивает самое старое.
Сообщение лежит поверх страницы и ничего в ней не двигает — строка в потоке увозила бы таблицу сначала вниз, а через десять секунд обратно.
Отдельный случай — данные, на которых страница не рисуется: загрузка тогда
откатывается целиком, состав возвращается к прежнему, а причина встаёт
рядом с именем файла. Перестановка и удаление откатываются по тому же
правилу, только причина уезжает в предупреждения — своего места рядом с
файлом у них нет. Разные koji_hub в снапшотах — не отказ, а
предупреждение: сравнивать такие снапшоты обычно бессмысленно, но переезд
хаба и зеркала бывают. Предупреждения всплывают тем же окошком и
показываются один раз: перестановка снапшотов их не повторяет — состав от
неё не меняется.
Загруженное живёт только до перезагрузки страницы. Никакого хранилища у дашборда нет — он ничего не пишет ни в браузер, ни на диск, и F5 возвращает пустую зону загрузки. Это цена того, что страница ничего о себе не помнит и файлом её можно передать кому угодно, ничего с собой не унося.
Отвечает на вопрос «что сейчас в теге»: карточки-счётчики (всего билдов, с патчами, унаследованных, проблемных, отдельно по каждому классу патчей) и таблица билдов с раскрытием строки — ссылки на Koji и GitLab, патчи по классам (со ссылками на файл), пакеты блоками по архитектуре, проблемы. Снапшот выбирают на рельсе над вкладками: клик по узлу открывает его, открытый узел залит и подсвечен. Отдельного переключателя нет — число билдов и имя файла каждого снапшота стоят в списке источников. При единственном снапшоте узел не нажимается: переключать не на что. Открывается вкладка на последнем снапшоте цепочки — при обычном порядке это самый свежий из загруженных.
Колонка «тег» показывает, откуда билд взялся: прочерк — билд затегован прямо в выбранный тег, имя — унаследован из этого родительского тега. Знак вопроса означает, что снапшот собран версией, которая тег ещё не записывала; «неизвестно» и «прямой» намеренно не смешиваются.
Раскрытая строка держится за свою: полоса у левого края идёт через строку и её детали насквозь, поэтому при нескольких раскрытых строках сразу видно, где чьи блоки. У билда с проблемой полоса красная — та же, что метит строку и без раскрытия.
Путь патча стоит второй строкой только тогда, когда он что-то добавляет к
имени. Обычно путь — это PATCH/<имя>, то есть имя, повторённое с
приставкой: строка вдвое длиннее, а нового в ней ноль. Показывается путь у
патчей из подкаталога — и тогда вторая строка сама работает сигналом «этот
лежит не там, где все», — и когда запрос поиска попал в путь, но не в имя:
скрыть строку, из-за которой патч оказался в выдаче, значило бы соврать,
почему он тут.
В раскрытии строки, в блоке koji, теги разведены по двум строкам.
«Основной тег» — тот, через который билд попал в этот снапшот: набран жирным
цветом текста и подписан (прямой) или (унаследован). «Другие теги» —
где тот же билд висит ещё; они приглушены, а когда таких тегов нет, строка
остаётся на месте с прочерком, чтобы блоки соседних раскрытых строк не
разъезжались. Поиск ищет и по другим тегам, так что запрос вида
os-9.2-candidate найдёт все билды, стоящие в этом теге.
В колонке «патчи» рядом с числом стоит полоска состава: цвет занимает долю, равную доле своего класса среди патчей этого билда. Длина полоски одна у всех строк — иначе доли нельзя сравнивать между билдами, а «сколько патчей» и так сказано числом слева. Класс, представленный одним патчем из сотни, всё равно виден: у сегмента есть пол в 2 пикселя, а точные числа показывает подсказка при наведении.
Колонка «собран» показывает дату и время в московском времени в два
уровня: дата первой строкой, время второй и бледнее. Колонку читают по
датам, секунды нужны, когда до строки уже дошли, а одной строкой она
держала под собой девятнадцать знаков ширины — те, которых не хватало
имени компонента слева: оно переносилось, и «NetworkManager» разрывался
посередине. Теперь имя не переносится вовсе. В раскрытии то же значение
стоит целиком, с пометкой МСК. Хаб отдаёт UTC, и в снапшоте хранится именно он — перевод делает сама
страница (toMsk в assets/js/viewmodel.js), так что один и тот же снапшот
всегда даёт один и тот же вид. Москва — UTC+3 круглый год, перехода на
летнее время в России нет с 2014, поэтому смещение задано числом
(MSK_SHIFT_MS) и считается через Date.UTC, а не через базу зон и не через
местный пояс: иначе дашборд показывал бы разное время на разных машинах.
Понадобится другая зона — меняется эта одна константа. Дата без часа
(снапшот прежней версии) не переводится: прибавив три часа к неизвестному
времени, дашборд утверждал бы то, чего не знает.
В колонке «владелец» стоит koji-логин того, кто запустил билд. Сортировка по ней собирает билды одного человека подряд.
Раскрытие — полная карточка билда, а не выжимка из того, чего нет в
строке: в блоке koji стоят NVR, теги, время сборки, владелец,
идентификаторы билда и задачи и ссылка на билд; в блоке gitlab — проект,
ветка, наличие каталога PATCH и ссылка. Поле, которое видно и в строке,
из карточки не выкидывается: сюда приходят, когда строки уже мало, и
заставлять читать в двух местах сразу незачем. Исключение одно — метки:
у них своя колонка, и второй такой же полосы в раскрытии нет.
Строка с веткой подписана по виду источника: «ветка», когда билд собран с
ветки, «коммит», когда с коммита, и «srpm», когда его собрали не из git, а
из готового SRPM. Значение в ней тогда не имя ветки, а хеш или имя файла, и
подписать его веткой значило бы соврать. У сборки из SRPM и сам блок зовётся
srpm, а не gitlab: GitLab в ней не участвовал вовсе, и пустые «проект» и
«ссылка» в нём — не недосмотр. То же в раскрытии «Изменений»: подпись у
каждой стороны своя, и пара «ветка → srpm» читается как есть.
Появляется, только когда на странице два и более снапшота. Сравнить можно любые два загруженных снапшота, а не только соседние по цепочке. Выбор — двумя кликами по узлам рельса, того же ряда снапшотов, что стоит над вкладками: первый клик отмечает узел кольцом, второй задаёт диапазон. Клик по отмеченному узлу снимает отметку. Пока отметка стоит, таблица не трогается — начатый выбор ни на что не влияет, пока не завершён вторым кликом.
Концы диапазона на рельсе залиты, а узлы между ними обведены вполсилы: белым — то, что сравнивается, серым — то, что стоит в стороне, и промежуточным — снапшоты, которые в сравнение не попали, но лежат в его сроке. Диапазон os-9.1 → os-9.5 не знает, что было в os-9.2, os-9.3 и os-9.4, и рельс об этом говорит.
Направление задаёт цепочка, а не порядок кликов: рельс упорядочен слева направо от старого тега к новому, и «было» — то, что левее. Кликнули третий узел, потом первый — получите переход от первого к третьему. Обратный порядок молча поменял бы местами «появился» и «исчез». Пока ничего не отмечено, открыт самый широкий диапазон — вся цепочка.
Переходы между соседними тегами и сводный по всей цепочке посчитаны заранее, при загрузке; любой другой диапазон считается в момент выбора. Заметной паузы это не даёт: это один дифф, столько же работы, сколько страница уже делает на загрузке для каждого соседнего перехода.
Диапазон можно назвать и ссылкой: pair= в адресе называет любые два
конца полными именами прогонов. Короткая форма по тегам,
pair=os-9.1..os-9.2, по-прежнему читается — она выбирает последний
диапазон, у которого теги стоят в написанном порядке: самый свежий левый
конец, у которого правый ещё есть где-то правее, а у него самого —
самый свежий из подходящих правых. Если в написанном порядке ссылка не
читается вовсе, порядок разворачивается по цепочке.
Первым рядом стоят четыре большие карточки — итоги перехода, тот же ряд, что на «Состоянии» занимают итоги тега: было и стало — сколько билдов в теге на каждом конце, разница — на сколько их стало больше или меньше, срок — сколько времени прошло между сборами.
Числа сторон берутся у самих снапшотов, а не по строкам таблицы: строка — это компонент перехода, и компонент, которого на этой стороне ещё или уже нет, в ней всё равно стоит. Под числом стороны — тег и время сбора: тега мало, два прогона одного тега законны, и «было os-9.4 → стало os-9.4» без времени сбора не сказало бы, какой из них какой. Ноль в разнице не значит «ничего не менялось»: сколько компонентов ушло, столько могло и прийти, — поэтому под ней стоят появившиеся и исчезнувшие по отдельности. Срок считается той же мерой, что подписывает отрезок рельса.
Фильтра у этих четырёх нет: итог перехода — не срез таблицы, а то, между чем считали, и кнопка обещала бы клик, которому нечего делать.
Ряды карточек занимают строку целиком, и последняя строка тоже. Браузер сам этого не делает: он набивает строку под завязку и про остаток не думает, поэтому одиннадцать срезов при десяти влезающих давали строку из одной карточки и пустоту за ней. Страница считает, на сколько строк карточки делятся поровну — одиннадцать по шесть это шесть и пять, — и задаёт ширину сама; на узком окне тем же правилом делятся и четыре больших итога, ложась два и два вместо трёх и одного. Пересчитывается это при смене снапшота, вкладки и размера окна.
Ниже — одиннадцать карточек-счётчиков диффа: агрегат «изменились»
(версия, патчи, состав RPM, ветка или тег — что угодно из перечисленного
ниже), «появились» (added), «исчезли» (removed), «версия выросла»
(upgraded), «версия упала» (downgraded), «версия та же» (unchanged —
но патчи или RPM могли измениться и тогда), «патчи пришли» (patches+),
«патчи ушли» (patches-), «состав RPM» (repackaged), «сменили ветку»
(branch-changed), «переехали между тегами» (tag-changed). Ниже —
таблица изменившихся компонентов с раскрытием «было / стало».
tag-changed ставится только тому билду, который сам остался прежним
(совпал NVR), а koji-тег у него поменялся: билд вытащили из родительского
тега и затеговали напрямую или наоборот. Обновление компонента такой метки
не получает — новый билд лежит в новом теге по определению, и про него уже
всё сказано меткой upgraded. Сравнивается сам тег билда, а не то, выглядит
ли он прямым из выбранного: иначе при os-9.2, наследующем os-9.1,
переехавшим оказался бы каждый нетронутый билд.
«Было» — это состояние, а не половина диффа. Слева стоит то, что было на тот момент: списки патчей и пакетов без единой пометки. Там ничего не происходило, и вычеркнутая строка утверждала бы, будто происходило.
Весь переход показан справа, в «стало». Уцелевшее набрано обычным,
ушедшее зачёркнуто и помечено − на своём прежнем месте среди уцелевших,
пришедшее помечено + и стоит внизу своей группы. Так одна колонка
отвечает и на «что теперь», и на «что с этим стало», а вторая остаётся
точкой отсчёта.
Класс патчей или архитектура, откуда ушло всё, остаётся в «стало» с нулём в счётчике и одной зачёркнутой строкой: «был и кончился» — тоже ответ. Счётчик блока везде считает новое состояние, зачёркнутое в него не входит.
Списки RPM разбиты на блоки по архитектуре (src, noarch, дальше
остальные по алфавиту). Подпакет сопоставляется по name.arch, без
version-release, — иначе обновление билда выглядело бы как полная замена
состава. Именно это сопоставление и решает, что зачеркнуть, а что дописать
внизу блока.
Сводка стороны — такая же полная карточка билда, как в «Состоянии»: версия, тег, время сборки, владелец, ветка, проект и ссылки на билд и на исходник. «Было» и «стало» — это две карточки одного компонента, снятые в разные моменты, и уходить из раскрытия за остальным человеку негде. Изменившееся помечено на стороне «стало» — тег, ветка, владелец, проект. Время сборки не помечается никогда: у пересобранного компонента оно разное всегда, и пометка на нём ничего не сообщала бы. У смены владельца и переезда проекта своей метки нет и фильтра по ним тоже: это подробность, которую видно, только когда строку уже раскрыли.
Раскрытие собрано парами: шапка к шапке, сводка к сводке, патчи к патчам, пакеты к пакетам — каждая пара стоит в своей строке сетки и потому имеет общую высоту. Иначе списки пакетов начинались бы на разной высоте: патчей слева три, справа пять — и правый уезжал бы вниз на два ряда.
Вычисляемые метки поверх строки, к koji-тегам отношения не имеют. Стоят в колонке «метки» — не «теги»: тег в этой таблице один, koji-тег билда, и он в своей колонке слева.
На вкладке «Состояние»: autogen, cve, sast, dast, coverage,
spec, changelog, files, other (класс патча, присутствует в билде
хотя бы один патч этого класса), inherited (билд
висит не в выбранном теге, а в одном из его родителей), no-patch (у ветки
нет каталога PATCH, т.е. patch_dir_present == false), no-source
(в билде нет extra.source.original_url), from-commit (билд собран не с
ветки, а прямо с коммита — источник это допускает, но диффа веток тогда не
будет), from-srpm (билд собран не из git, а из готового SRPM: ветки у
него нет, каталог PATCH читать негде, и патчей в строке не будет —
это не поломка, а другой способ собрать), gitlab-error (репозиторий, ветка или каталог патчей недоступны —
включает и bad source url, и любой gitlab: ..., включая 401/403 без
токена), internal-error (сбор данных по билду упал с неожиданной
исключительной ситуацией — сама ошибка не роняет весь прогон, а
записывается в problems билда).
На вкладке «Изменения»: added, removed, unchanged, upgraded,
downgraded (статус компонента) и, если применимо, patches+, patches-,
repackaged (изменился список RPM без смены EVR), branch-changed,
tag-changed.
У каждого признака три положения: неважно, есть и нет. Ставят их в меню под кнопкой «Фильтры» — она же показывает, сколько условий стоит сейчас, и горит, пока хоть одно стоит. Клик по карточке-счётчику или по метке строки по-прежнему ставит и снимает фильтр, только двумя положениями из трёх: «есть» и «неважно». Отрицание ставят в меню; карточка его показывает — приглушается и обводится цветом убранного, — а клик по ней снимает.
Признаки разложены по группам: на «Состоянии» это классы патчей, свойства билда и проблемы, на «Изменениях» — статус и что изменилось. У каждой группы переключатель все / любой из. «Все» — умолчание и то же самое, что было раньше: складывать по И. «Любой из» складывает по ИЛИ отмеченное как «есть» внутри одной группы; отмеченное как «нет» остаётся запретом при любом положении переключателя — «нет autogen» значит «точно не autogen», и складывать такое по ИЛИ незачем. Группы между собой всегда по И.
Так задаётся запрос вроде «есть CVE-патч и при этом нет autogen-патча»: два клика в группе классов. Рядом с каждым признаком стоит число — сколько строк подходит под него самого, без оглядки на остальные фильтры.
Поле поиска ищет по имени компонента, NVR, koji-тегу билда, ветке, именам
файлов патчей, CVE-ID и именам RPM. Текущий вид — активная вкладка, выбранный
снапшот («Состояние») или переход («Изменения»), фильтры, поисковый запрос,
сортировка — синхронизируется с location.hash.
Снапшот стоит в хеше именем — тегом и временем сбора, — а переход именами
обоих концов: номер после перестановки или удаления показал бы другой
снапшот, ничем не выдав подмены, а одного тега мало, когда на странице два
прогона одного тега. Имя уезжает в адрес экранированным, так что глазами
человек видит tag=os-9.2%402026-08-01T00%3A00%3A00%2B03%3A00, а не
os-9.2@2026-08-01T00:00:00+03:00; читать и править удобнее короткую форму
tag=os-9.2 — она тоже принимается. Двойников короткая форма не различает
и выбирает последний подходящий снапшот по цепочке: при обычном порядке это
самый свежий, а после ручной перестановки — тот, кого человек поставил
последним. Так же читается и короткая форма перехода, os-9.1..os-9.2.
Фильтр из хеша, который на живых данных не опознаётся, молча выбрасывается:
иначе страница показывала бы пустую таблицу под фильтр, которого нет ни на
одной карточке — его нечем было бы снять. Проверка идёт по трём спискам:
постоянные подписи самой страницы, классы патчей загруженных снапшотов и
метки строк того снапшота или перехода, который сейчас показан. Постоянные
подписи — это в том числе статусы диффа, и они
проходят проверку всегда, независимо от вкладки: #tab=state&f=downgraded
разбор переживёт и даст на «Состоянии» пустую таблицу. Довод это не рушит —
такой фильтр виден в меню и снимается оттуда, — но правило именно такое.
Фильтры в ссылке: f=cve,-autogen — минус перед ключом значит «нет»,
any=classes — список групп, переключённых в «любой из». Ссылка без минусов
и без any= открывает тот же срез, что и до появления трёх положений.
Ссылку на срез можно скопировать и переслать, но данных она не несёт: тот, кто её откроет, сперва подгружает те же снапшоты, и только тогда вид восстановится. Это прямое следствие того, что страница и данные разделены.
Всё логирование настраивается одним общим флагом --log-level
(error/warning/info/debug, по умолчанию info) и идёт в stderr:
| Уровень | Что в нём видно |
|---|---|
error |
только фатальные ошибки — та самая одна строка перед кодом возврата 2 |
warning |
плюс проблемы отдельных билдов (то, что попадает в problems снапшота и метка gitlab-error/internal-error в дашборде), повторы запросов к GitLab после 429/5xx, отсутствие multicall на старом хабе koji (сбор идёт последовательными вызовами вместо пакетных), превышение --max-problems |
info (по умолчанию) |
плюс имя записанного файла (у обеих подкоманд), а у collect — ещё и параметры прогона (хаб, теги, --jobs, задан ли токен), размер тега, прогресс сбора примерно на каждые 5% билдов, итоговая сводка по тегу, общее время |
debug |
плюс каждый запрос к GitLab (URL, код ответа, длительность, тело ответа при ошибке) и каждый вызов koji (метод, число билдов в пакете, длительность — у XML-RPC своих URL и кода ответа нет), попадания в кэш дерева патчей, размер собранной страницы, полный traceback фатальной ошибки |
Пример строки в обычном формате (info и выше):
23:08:22 INFO cli: написан dashboard.html
На debug формат тот же, но с именем потока — при --jobs больше единицы
сбор идёт в пуле потоков, и без имени в строке не разобрать, какой запрос
к какому потоку относится:
23:08:53 DEBUG [w_0] gitlab: GET https://gitlab.example.com/api/v4/projects/g%2Fpkg1/repository/tree ref=br path=PATCH → 200 за 0.00 с
Логи всегда идут в stderr, а не в stdout, поэтому -o (файл снапшота или
страницы) и 2>run.log (файл журнала) друг другу не мешают:
python3 -m dashboard --config dashboard.yaml --log-level debug collect \
--tag os-9.2 -o os-9.2.json 2>run.log--log-level debug на теге в несколько сотен билдов даёт тысячи строк — это
ожидаемо, уровень для разбора конкретной проблемы (например, почему билд
получил gitlab-error), а не для повседневного запуска.
Токен GitLab (см. раздел выше) ни на одном уровне, включая debug, в лог
не попадает: в строку параметров прогона пишется только задан/не задан,
а из параметров HTTP-запроса в лог уходят ref/path/page, но не
заголовки.
| Код | Когда |
|---|---|
| 0 | успех |
| 1 | только для collect: суммарно по всем собранным в этом прогоне снапшотам проблемных билдов больше, чем --max-problems |
| 2 | фатальная ошибка: конфиг не читается или не проходит валидацию, koji недоступен на уровне транспорта, шаблон страницы или скрипт к ней не читается, ошибка ввода-вывода и т. п. |
--max-problems без значения (флаг не передан) отключает проверку и код 1
не возвращается никогда. Считаются проблемные билды только что собранных
снапшотов, поэтому проверка возможна лишь у collect; page
возвращает 0 сразу после успешной записи файла — снапшотов он не видел и
считать ему нечего.
Разбор снапшотов кодом возврата больше не отражается вовсе: их читает страница, а не CLI, и негодный файл она называет на экране, рядом с зоной загрузки (см. «Как в него попадают снапшоты»).
Во всех случаях кода 2 пользователю на уровне error печатается одна строка
вида <описание>: <исключение> — конфиг ли не читается, не собирается ли
страница, ошибка ли это ввода-вывода, или что-то совсем непредвиденное (koji
недоступен на уровне транспорта и т. п.). Полный traceback этой же ошибки
печатается дополнительно, но только на --log-level debug — см. «Логи».
Файл, который пишет collect и читает страница дашборда, — JSON-массив
снапшотов (или один объект снапшота — обе формы понимает и страница, и
питоновская модель). Пример на одном билде:
[
{
"schema": 1,
"dashboard": "1.0.0",
"tag": "os-9.1",
"generated": "2026-07-01T00:00:00+03:00",
"koji_hub": "https://hub/kojihub",
"koji_web": "https://hub/koji",
"patch_classes": ["AUTOGEN", "CVE", "SAST", "DAST", "COVERAGE",
"DISTSUFFIX", "LICENSE", "SPEC", "CHANGELOG", "FILES",
"other"],
"builds": [
{
"nvr": "nginx-1.24.0-1.el9",
"name": "nginx",
"version": "1.24.0",
"release": "1.el9",
"epoch": null,
"build_id": 1,
"task_id": 2,
"owner": "builder",
"completed": "2026-05-14 10:00:00",
"tag_name": "os-9-base",
"tags": ["os-9-base", "os-9.1"],
"source": {
"raw": "git+ssh://git@h/g/nginx?#origin/main",
"host": "h",
"project": "g/nginx",
"ref": "main",
"ref_kind": "branch",
"web_url": "https://gl/tree"
},
"patch_dir_present": true,
"patches": [
{
"path": "PATCH/CVE-2024-7347.patch",
"name": "CVE-2024-7347.patch",
"class": "CVE",
"cves": ["CVE-2024-7347"],
"web_url": "https://gl/blob/CVE-2024-7347.patch"
}
],
"rpms": ["nginx-1.24.0-1.el9.x86_64"],
"problems": []
}
]
}
]schema — версия формата (сейчас 1); файл с другим значением страница
не примет и скажет об этом прямо, назвав чужую версию, а не «это не
снапшот»: такой файл сделан другой версией dashboard, и человеку полезнее
знать какой.
dashboard — версия инструмента, записавшего файл. Поле необязательное:
снапшоты, собранные до его появления, читаются как прежде, и схему оно не
меняет — версия формата и версия инструмента растут порознь. Отвечает оно
на вопрос «чем это собрано», который возникает, когда файл прислали со
стороны. Та же версия стоит в собранной странице — в <meta name="generator"> и рядом с заголовком, — и печатается по --version.
Что менялось от версии к версии, сказано в CHANGELOG.md.
generated — время сбора этого снапшота, в местной зоне машины, где шёл
collect. Оно же — половина имени снапшота: пара «tag и generated»
отличает два прогона одного тега друг от друга, по ней страница выстраивает
цепочку, ловит повторную загрузку и называет снапшот в адресной строке.
patch_classes — имена классов патчей в порядке правил классификатора.
Дашборду этот порядок нужен для карточек классов, меток строк и разбора
фильтров из ссылки, а взять его больше неоткуда: конфига у страницы нет.
Когда снапшотов несколько, список задаёт первый из них, а остальные могут
только дописать в конец то, чего в нём не было.
Поле необязательное: снапшоты, собранные до его появления, читаются по-прежнему. Но выведет классы из самих патчей (по алфавиту) страница только тогда, когда поля нет ни у одного загруженного снапшота — иначе список берётся у тех, кто его несёт, и всё. Смешанный набор — старый снапшот без поля рядом с новым — это ровно тот случай из «Быстрого старта», где тег сравнивают с ним же месяц назад: список страницы будет списком нового снапшота, а класс, который встречается только в старом, в него не попадёт. Совсем такой класс не пропадает: пока выбран старый снапшот, у него есть карточка — хвостом, за перечисленными, — и метку в строке он тоже получит. Но в списке классов страницы его не будет, а фильтр по нему из присланной ссылки опознается только там, где такие строки видны: единственной опорой остаются метки строк выбранного снапшота. Собрать оба снапшота одной версией dashboard дешевле, чем разбираться в такой странице.
completed — время окончания сборки в виде YYYY-MM-DD HH:MM:SS, в
UTC, как его отдаёт хаб: снапшот хранит то, что сказал koji, а в
московское время значение переводится уже на странице. Доли секунды и
смещение зоны срезаются. Снапшоты, собранные до появления времени, несут
одну дату — дашборд с ними работает как прежде, просто без часов.
tag_name — koji-тег, в котором билд действительно затегован (его отдаёт
listTagged); совпал с tag снапшота — билд прямой, не совпал —
унаследован оттуда. Поле необязательное: снапшоты, собранные до его
появления, читаются по-прежнему, и такие билды показываются как «тег
неизвестен», а не как прямые. tags — все koji-теги билда (их отдаёт
listTags), включая tag_name; в дашборде они показаны в раскрытии строки,
а tag_name среди них выделен. Поле тоже необязательное: пустой список
означает «не спрашивали», и дашборд тогда показывает только то, что знает
из listTagged.
koji_hub/koji_web снапшота — не текущий конфиг, а то, чем реально
пользовался collect в момент сбора этого конкретного снапшота. Страница
берёт koji_web у того снапшота, из которого пришёл показанный билд: на
«Состоянии» — у снапшота своей строки, на «Изменениях» — у той стороны пары,
откуда взят билд. Так ссылки не уводят на чужой хаб, когда рядом легли
снапшоты разных прогонов. Сторона пары ищется по имени тега, поэтому у двух
прогонов одного тега адрес возьмётся у первого из них — заметно это, только
если у прогонов разный koji_web. Разные koji_hub страница ловит
предупреждением, koji_web не сравнивается.
source — откуда билд собран, разобранный extra.source.original_url.
Вид источника говорит ref_kind: branch — из ветки, commit — прямо с
коммита (в ref тогда хеш), srpm — не из git, а из готового SRPM (в ref
имя файла, host и project пусты, ходить в GitLab не за чем), none —
ссылка разобрана, но ветки в ней нет. Само поле необязательное: у билда без
original_url его нет вовсе, и в строке стоит метка no-source.
patch_dir_present трёхзначен:
true— каталогPATCHв ветке прочитан (список файлов, пусть и пустой, получен);false— ветка существует, но каталогаPATCHв ней нет;null— не определялся или определить не удалось: у билда вовсе нет источника, ссылка на него не разбирается, GitLab ответил ошибкой — или билд собран из готового SRPM, и каталог читать негде. В первых трёх случаях причина всегда есть вproblems; в последнемproblemsпуст, а вид источника говоритref_kind: "srpm"и меткаfrom-srpmв строке.
false и null в этом случае клиент различает вторым запросом. Ответ 404
на запрос дерева PATCH сам по себе неоднозначен: так отвечает и «в ветке
нет каталога PATCH», и «ветки уже нет», причём формулировка зависит от
версии GitLab — встречаются 404 Tree Not Found, 404 invalid revision or path Not Found, причина в поле error вместо message и вовсе пустое тело.
Поэтому решает не текст ответа: любой 404, кроме явного 404 Project Not Found, уточняется запросом GET /repository/commits/<ref>. Если коммит
найден — ветка существует, каталога нет, patch_dir_present = false и никакой
проблемы не записывается. Если и там 404 — problems: ["gitlab: ref not found"] и patch_dir_present = null, а не молчаливое «патчей нет».
404 Project Not Found распознаётся сразу и второго запроса не вызывает.
problems — список текстовых причин, с которыми накопитель столкнулся по
конкретному билду; префиксы значимы и определяют теги в дашборде (см. выше):
no source url, bad source url: ..., gitlab: ... (в том числе
gitlab: no ref in source url, gitlab: unknown host,
gitlab: ref not found, gitlab: 401 .../gitlab: 403 ... без валидного
токена), internal error: ....
Питон собирает данные, страница их показывает — по этой границе разложены и файлы.
| Что | Где |
|---|---|
| разбор командной строки | dashboard/cli.py |
| конфиг, классификатор, модель снапшота, логи | config.py, classify.py, model.py, logs.py |
| сбор данных | collect.py, kojiclient.py, gitlabclient.py, httpclient.py, sourceurl.py |
| сборка страницы | build.py + assets/dashboard.html + assets/css/*.css + assets/js/*.js |
httpclient.py держит сам разговор по HTTP — повторы, паузы, Retry-After
и вычистку токена из сообщений об ошибках. gitlabclient.py знает только
про GitLab: как спросить дерево ветки, как отличить «каталога нет» от
«сервер не ответил» и как собрать веб-ссылку.
build.py не считает ничего: он читает шаблон, подставляет вместо двух
плейсхолдеров стили и скрипты и отдаёт один файл.
Восемь файлов в assets/css/, каждый про свой участок страницы. Порядок
важен: при равной специфичности выигрывает то, что ниже, поэтому он задан
в STYLES и сторожится тестом.
| Файл | За что отвечает |
|---|---|
base.css |
переменные, сброс, типографика, палитра классов патчей |
layout.css |
шапка, панель источников, экран загрузки, вкладки, панель управления, кнопка «наверх» |
rail.css |
рельс цепочки: узлы, отрезки, крестики, призрак |
cards.css |
карточки-счётчики |
table.css |
таблицы обеих вкладок и всё, что раскрывается под строкой |
filters.css |
кнопка фильтров, плашка меню, тройной переключатель |
toasts.css |
стопка всплывающих сообщений в правом нижнем углу |
tip.css |
всплывающая подсказка |
Восемнадцать файлов в assets/js/. Порядок в SCRIPTS задан по
зависимостям, а не по алфавиту: каждый следующий рассчитывает, что
предыдущие уже положили себя в KP.
Разложены они по тому, чем модуль владеет, а не по тому, к какой вкладке относится.
Чистые — данные приходят доводами, наружу уходят строки. Ни DOM, ни состояния страницы они не видят и проверяются без заглушки браузера.
| Файл | За что отвечает |
|---|---|
vercmp.js |
сравнение версий по правилам rpm |
rpms.js |
архитектура пакета и порядок RPM |
diff.js |
сравнение снапшотов и цепочка пар |
viewmodel.js |
данные страницы: строки, счётчики, метки, перевод времени |
text.js |
экранирование, подсветка запроса, склонение, время |
labels.js |
как страница называет ключи из данных |
hash.js |
разбор и сборка строки адреса |
markup.js |
куски разметки, общие обеим таблицам |
tables.js |
строки и детали таблиц |
cards.js |
карточки-счётчики |
Владельцы участков DOM — у каждого свой узел и свои обработчики, навешенные один раз.
| Файл | За что отвечает |
|---|---|
filters.js |
кнопка фильтров и меню под ней |
rail.js |
рельс: разметка, выбор снапшота и диапазона, перестановка |
files.js |
загрузка снапшотов файлами |
tips.js |
подсказки |
toasts.js |
всплывающие сообщения: окошки в углу, свои таймеры |
Состояние и корень.
| Файл | За что отвечает |
|---|---|
store.js |
загруженные снапшоты: разбор, порядок, дубликаты, откат |
page.js |
состояние страницы и всё, что из него считается |
ui.js |
находит узлы, кладёт в них разметку, разводит события |
page.js заводится фабрикой, а не живёт синглтоном: тест поднимает свежее
состояние одним вызовом. ui.js — единственный, кто знает и про DOM, и про
всех остальных; перерисовку он раздаёт владельцам участков объектом app.
Раньше всё это считал питон и запекал результат внутрь HTML. Так можно, пока
набор снапшотов известен в момент сборки страницы, — но человек складывает
файлы рядом уже после того, как страница написана, и дифф между двумя
произвольно подгруженными снапшотами взять готовым неоткуда. Считать его
может только страница, и раз уж вычисления всё равно оказались в браузере,
держать вторую их копию в питоне было бы обещанием, которое некому
проверить. Поэтому render.py, diff.py, rpms.py и rpmvercmp.py из
проекта ушли, а с ними и подкоманды render и run.
Гитфлоу в облегчённом виде: разработчик один, и ветка на стабилизацию релиза простаивала бы пустой.
| Ветка | Откуда | Куда вливается | Что в ней |
|---|---|---|---|
master |
— | — | то, что выпущено; каждый её коммит помечен тегом vX.Y.Z |
develop |
master |
master при релизе |
то, что готово, но ещё не выпущено |
feature/* |
develop |
develop |
одна задача |
hotfix/* |
master |
master и develop |
срочная починка выпущенного |
Задача живёт в своей feature/* и вливается в develop слиянием с
--no-ff: отдельный коммит слияния оставляет в истории видимую границу
задачи, а перемотка её потеряла бы.
Релиз — слияние develop в master и тег vX.Y.Z на нём. Номер к этому
моменту уже стоит в dashboard/__init__.py: его поднимают вместе с самим
изменением (см. ниже), а тег лишь отмечает, что именно этот номер вышел.
hotfix/* идёт от master, поднимает младший номер и вливается в обе
ветки — иначе починка потеряется в следующем релизе.
Номер живёт в dashboard/__init__.py и поднимается тем же коммитом, что и
само изменение, — не отдельным «релизным». Сломали флаг CLI или формат
снапшота — старший номер, появилась возможность — средний, починили или
поправили вид — младший; правки только в тестах, документации или
комментариях номер не двигают. Вместе с номером в CHANGELOG.md заводится
запись о том, что увидит человек, который этим пользуется. Что запись есть,
сторожит tests/test_version.py: покрасневший тест означает забытый
CHANGELOG, а не сломанный код.
Наборов тестов два — по разные стороны той же границы:
python3 -m unittest discover -s tests -v
node --test tests/js/*.test.jsПитоновский набор проверяет сбор данных и CLI, набор для Node — всё, что
делает страница; второму нужен Node со встроенным --test (18 и новее).
Оба набора без внешних сетевых зависимостей: koji и GitLab в питоновских
тестах подменены фейками из tests/fakes.py, а браузерным скриптам хватает
заглушки DOM из tests/js/domstub.js — она строит дерево из настоящего
assets/dashboard.html, поэтому пропавший в шаблоне id роняет тест, а не
молча ломает страницу. Сколько тестов прошло, скажут сами unittest и
node --test в последних строках вывода.
Отдельно стоит tests/js/fixtures/page-data.golden.json — эталон данных
страницы, порождённый ещё питоновским render.py на фикстурах
tests/fixtures/rich-old.json и rich-new.json — только на этих двух,
без остальных шести: те появились позже и нужны глазам, а
не сверке. С ним
значение в значение
сверяется buildPageData, и это сторож переноса вычислений в браузер: пока
эталон совпадает, страница считает ровно то же, что считал питон. Пересчитать
его больше нечем — render.py и tests/test_parity.py удалены вместе с
питоновским слоем представления, — поэтому эталон правят руками и только
осознанно, когда изменение данных страницы задумано. Тест рядом с ним
следит, чтобы в эталоне остались интересные случаи (все статусы диффа,
tag-changed, repackaged, branch-changed, билды с неизвестным и с
унаследованным тегом): без этой проверки обеднение фикстур прошло бы
незаметно и сверка стала бы сверкой ни с чем.
Остальные шесть смотрят глазами, но без присмотра не оставлены:
tests/test_fixtures.py проверяет, что каждый файл читается, что в нём
остался случай, ради которого он заведён, и что правит их
tests/fixtures/make_rich_fixtures.py, а не рука. Генератор запускают из
корня репозитория, и он переписывает только те файлы, в которых изменились
данные: версию записавшего каждый снапшот несёт своё, и на подъёме номера
фикстуры не трогаются.